#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os, os.path, time
import md5

import ConfigParser

import log
import key

from range import Range
from help import bisectLeft,insort

class   RangeNotExistError(Exception) : pass
class   RangeCorruptError(Exception) : pass
class   RangePropertyError(Exception) : pass

#######################################################################

always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
               'abcdefghijklmnopqrstuvwxyz'
               '0123456789' '_.-')
safe_map = {}

def quote(s) :
    """quote('abc def') -> 'abc%20def'."""
    if not safe_map :
        for i in range(256) :
            c = chr(i)
            safe_map[c] = (c in always_safe) and c or ('%%%02X' % i)
    res = map(safe_map.__getitem__, s)
    return ''.join(res)
    # quote()

_hextochr = dict(('%02x' % i, chr(i)) for i in range(256))
_hextochr.update(('%02X' % i, chr(i)) for i in range(256))

def unquote(s):
    """unquote('abc%20def') -> 'abc def'."""
    res = s.split('%')
    for i in xrange(1, len(res)):
        item = res[i]
        try:
            res[i] = _hextochr[item[:2]] + item[2:]
        except KeyError:
            res[i] = '%' + item
        except UnicodeDecodeError:
            res[i] = unichr(int(item[:2], 16)) + item[2:]
    return "".join(res)

#######################################################################

class   RangeServer(object) :
    '''

    root_dir : location where all ranges reigned by this
    '''
    def __init__(self, root_dir) :
        self.root_dir = root_dir
        self.id = md5.new(self.root_dir).hexdigest()
        self._tables = {}
        # __init__()

    def startup(self) :
        pass
        # startup

    def shutdown(self) :
        pass
        # shutdown

    ###################################################################

    def createRange(self, table_name, begin_row_key=key.ROW_KEY_MIN, end_row_key=key.ROW_KEY_MAX) :
        range_dir = self.getRangeDirName(table_name, begin_row_key)
        if not os.path.exists(range_dir) :
            os.makedirs(range_dir)
        self.setRangeProperties(table_name, begin_row_key, end_row_key)
        # createRange()

    def dropRange(self, table_name, begin_row, end_row=None) :
        range_dir = self.getRangeDirName(table_name, begin_row)
        if not os.path.exists(range_dir) :
            return True
        items = os.listdir(range_dir)
        for item in items :
            item = '/'.join((range_dir, item))
            if os.path.isfile(item) :
                os.remove(item)
            elif os.path.islink(item) :
                os.remove(item)
            elif os.path.isdir(item) :
                self.removeDirs(item)
            else :
                log.error('Unknown type entry "%s".', item)

        if os.path.exists(range_dir) :
            os.removedirs(range_dir)
        return True
        # dropRange()

    def openRange(self, table_name, begin_row, end_row=None) :
        table_loaded = False
        range_name = self.getRangeName(table_name, begin_row)
        if table_name in self._tables :
            _ranges = self._tables[table_name]
            _index = bisectLeft(_ranges, (begin_row, None))
            if _index < len(_ranges) :
                table_loaded = True
            else :
                #raise RangeNotExistError, 'range "%s" dose NOT exist.' % range_name
                table_loaded = False

        if not table_loaded :
            # range not load
            range_dir = self.getRangeDirName(table_name, begin_row)
            _range = Range(range_name, range_dir)
            _range.open()

            if table_name in self._tables :
                _ranges = self._tables[table_name]
                _ranges.insort( (begin_row, _range) )
                _index = bisectLeft(_ranges, (begin_row, None))
            else :
                self._tables[table_name] = [ (begin_row, _range) ]
                _ranges = self._tables[table_name]
                _index = 0

        _begin_row, _range = _ranges[_index]
        if not _range.opened :
            _range.open()

        return True
        # openRange()

    def closeRange(self, table_name, begin_row, end_row=None) :
        if table_name in self._tables :
            _ranges = self._tables[table_name]
            _index = bisectLeft(_ranges, (begin_row, None))
            if _index < len(_ranges) :
                _begin_row, _range = _ranges[_index]
                _range.close()

        return True
        # closeRange()

    def hasRange(self, table_name, begin_row, end_row=None) :
        try :
            _begin_row, _end_row = self.getRangeInterval(table_name, begin_row)
            if _begin_row == begin_row and (end_row is None or _end_row == end_row) :
                return True
        except :
            log.excpt()
            return False

        return False
        # hasRange()

    def getRangeInterval(self, table_name, begin_row) :
        properties = self.getRangeProperties(table_name, begin_row)
        _table_name, _begin_row, _end_row = properties

        return (_begin_row, _end_row)
        # getRangeInterval()

    def getRangeName(self, table_name, begin_row) :
        range_name = '%s.%s' % (table_name, quote(begin_row))
        return range_name
        # getRangeName()

    def getRangeDirName(self, table_name, begin_row) :
        range_dir = '%s/%s.%s' % (self.root_dir, table_name, quote(begin_row))
        return range_dir
        # getRangeDirName()

    def getRangePropertiesFileName(self, table_name, begin_row) :
        range_dir = self.getRangeDirName(table_name, begin_row)
        properties_file = '%s/properties' % range_dir
        return properties_file
        # getRangePropertiesFileName()

    def getRangeProperties(self, table_name, begin_row) :
        range_dir = self.getRangeDirName(table_name, begin_row)
        if not os.path.exists(range_dir) :
            raise RangeNotExistError, 'range "%s.%s" dose NOT exist.' % (table_name, `begin_row`[1:-1])
        properties_file = self.getRangePropertiesFileName(table_name, begin_row)
        if not os.path.exists(properties_file) :
            raise RangeCorruptError, '"%s" does NOT exist.' % properties_file
        parser = ConfigParser.ConfigParser()
        parser.read(properties_file)
        section_range = 'range'
        _table_name = parser.get(section_range, 'table')
        _begin_row  = parser.get(section_range, 'begin')
        _end_row    = parser.get(section_range, 'end')
        if _begin_row : _begin_row = unquote(_begin_row)
        if _end_row   : _end_row   = unquote(_end_row)
        if _table_name != table_name or _begin_row != begin_row :
            raise RangePropertyerror, '"%s" describe (%s, %s, %s)' % (properties_file, _table_name, `_begin_row`[1:-1], `_end_row`[1:-1])

        return (_table_name, _begin_row, _end_row)
        # getRangeProperties()

    def setRangeProperties(self, table_name, begin_row, end_row) :
        range_dir = self.getRangeDirName(table_name, begin_row)
        if not os.path.exists(range_dir) :
            os.makedirs(range_dir)
        properties_file = self.getRangePropertiesFileName(table_name, begin_row)
        fh = open(properties_file, 'w')
        s = '[range]\ntable = %s\nbegin = %s\nend = %s\n' % (table_name, quote(begin_row), quote(end_row))
        #log.debug('\n%s', s)
        fh.write(s)
        fh.close()
        # setRangeProperties()

    def removeDirs(self, dir_name) :
        items = os.listdir(dir_name)
        for item in items :
            dir_item = '%s/%s' % (dir_name, item)
            if os.path.isdir(dir_item) :
                self.removeDirs(dir_item)
            else :
                os.remove(dir_item)
        if os.path.exists(dir_name) :
            os.removedirs(dir_name)
        # removedirs()

    ###################################################################

    def lookupRange(self, table_name, row_key) :
        if table_name not in self._tables :
            raise TableNotExistError, 'Table "%s" does NOT exist.' % table_name

        _ranges = self._tables[table_name]
        if not _ranges :
            raise RangeNotExistError, 'No ranges for table "%s".' % table_name
        # key_to_search : (row_key, '') work
        # key_to_search : (row_key, None) does NOT work
        index = bisectLeft(_ranges, (row_key, '') )
        log.debug('Ranges=%s row_key=%s index=%d', `_ranges`, `row_key`, index)
        return  _ranges[index][1]
        # lookupRange()

    ###################################################################

    def insert(self, table_name, row_key, columns) :
        '''
        insert row_key and columns into one of ranges reigned by this server

        if row_key is out-of-range of all ranges, throw IndexError
        '''
        _range = self.lookupRange(table_name, row_key)
        _range.insert(row_key, columns)
        # insert()

    def erase(self, table_name, row_key, columns=None) :
        '''
        erase row_key (row or columns) from one of ranges reigned by this server

        if row_key is out-of-range of all ranges, throw IndexError
        '''
        _range = self.lookupRange(table_name, row_key)
        _range.erase(row_key, columns)
        # erase()

    ###################################################################

    class   Scanner(object) :
        def __init__(self, range_server) :
            self.range_server = range_server
            # __init__()

        def __iter__(self) :
            return self
            # __iter__()

        def next(self) :
            raise StopIteration
            # next()
        # class Scanner

    def createScanner(self, table_name, context, timeout=3600) :
        scanner = RangeServer.Scanner(self)

        return scanner
        # createScanner

    # class RangeServer


#######################################################################

